PostgreSQL 数据库集簇和表
1 背景知识
本文介绍 PostgreSQL 数据库集簇、数据库、数据对象。
2 数据库集簇的逻辑结构
2.1 数据库集簇
数据库集簇(database cluster)从逻辑方面来说是一组数据库(database) 的集合,由一套 PostgreSQL 服务节点进行管理。
请注意有些同学对这个定义也许会疑惑,这里要强调一点:
PostgreSQL 中的术语 数据库集簇
,并非 是指很多数据库服务器节点。
2.2 数据库(database)
- 数据库(database) 是 数据库对象(database objects) 集合。
- 在关系型数据库理论中,数据库对象是用于存储或查询数据的数据结构。
- (堆)表是一个典型的例子,还有更多种对象,例如索引,序列,视图,函数等。
- 在 PostgreSQL 中数据库(database)本身也是数据库对象,并在逻辑架构上彼此分离。
- 所有其他的数据库对象(例如表,索引等)归属于各自对应的数据库。
下图展示了一个数据库集簇的逻辑结构。
2.3 模式(schema)
PostgreSQL 数据库提供一个默认名为 public
模式。也可以创建自己的模式。用于在同一个数据库中的区分不同的数据库对象。
2.4 数据库对象
- 在关系型数据库理论中,数据库对象是用于存储或查询数据的数据结构。
- 数据库对象包括数据库、索引、序列、表、存储过程、函数等结构。
- (堆)表是一个典型的例子,堆表在逻辑上时 PostgreSQL 数据库存储数据和读取数据的存储结构。
- 数据库对象(例如表,索引等)归属于各自对应的数据库。
- 数据库(database) 也被称之为一个对象,叫做数据库对象。
2.5 数据库对象标识符(Object Identifiers, OID)
在 PostgreSQL 内部,所有的数据库对象都通过相应的对象标识符(Object Identifiers, OID) 进行管理,对象标识符(Object Identifiers, OID) 是无符号的 4
字节(bytes)整数类型。数据库对象与相应 对象标识符(Object Identifiers, OID) 之间的对应关系存储在相应的数据字典 中。
例如,数据库
和 堆表对象
的 对象标识符(Object Identifiers, OID) 分别存储在 pg_database 和 pg_class 中。
2.5.1 对象标识符(Object Identifiers, OID) 和数据库对象的对应关系
因此当你想要找到某个对象的OID时,可以执行以下查询:
SELECT datname, oid FROM pg_database WHERE datname = 'testdb';
datname | oid
---------+-------
testdb | 16384
(1 row)
SELECT relname, oid FROM pg_class WHERE relname = 'film';
relname | oid
---------+-------
film | 16631
(1 row)
SELECT pg_relation_filepath('film');
pg_relation_filepath
----------------------
base/16384/16631
(1 row)
SELECT 'film'::regclass::oid;
oid
-------
16631
(1 row)
2.5.2 对象标识符(Object Identifiers, OID) 数值大小
由于对象标识符(Object Identifiers, OID) 是无符号的 4
字节(bytes)整数类型。所以OID 的最大大小为 42亿
,计算公式如下:
- 232 =4294967295=42 亿
2.6 数据库模版
PostgreSQL 数据库默认提供两个模版数据库:
template0
:为非默认的模板数据库,不可以修改。
template1
: 为默认的模板数据库,可以修改。创建新数据库时,将会根据此数据库为模版创建。
3 数据库集簇的物理结构
3.1 数据库集簇
数据库集簇从物理结构来说本质上就是一个文件目录。称之为 数据库集簇目录(data directory)
。此目录包含着一系列子目录与文件。
执行 initdb 命令之后会在 指定目录
下创建 数据集簇目录
。从而初始化一个新的数据库集簇。
通常为了方便起见将 数据集簇目录
的路径配置到环境变量 PGDATA
中。
下图展示了一个 PostgreSQL 数据库集簇的例子。 base
子目录中的每一个子目录都对应一个数据库。
数据库中每个表和索引都会在相应子目录下存储为(至少)一个文件;还有几个包含特定数据的子目录,以及配置文件。虽然 PostgreSQL 支持 表空间 ,但该术语的含义与其他 RDBMS 不同。 PostgreSQL 中的表空间对应一个包含基础目录之外数据的目录。
官方文档中描述了数据库集簇 的文件和目录的分布情况。下表中列出了主要的文件与子目录:
基本目录下的数据库文件和子目录的布局(参考官方文档)
文件 | 描述 |
---|---|
PG_VERSION |
包含PostgreSQL主版本号 |
pg_hba.conf |
控制PosgreSQL客户端认证 |
pg_ident.conf |
控制PostgreSQL用户名映射 |
postgresql.conf |
配置参数 |
postgresql.auto.conf |
存储使用ALTER SYSTEM 修改的配置参数(9.4或更新版本) |
postmaster.opts |
记录服务器上次启动的命令行选项 |
子目录 | 描述 |
---|---|
base/ |
default 表空间的物理目录 |
global/ |
global 表空间的物理目录。 |
pg_commit_ts/ |
事务提交的时间戳数据(9.5及更新版本)。 |
3.2 自定义表空间
PostgreSQL中的表空间(Tablespace) 是 base
目录之外的附加数据区域。在PostgreSQL 8.0
版本中引入了该功能。
下图展示了表空间的内部布局,以及表空间与主数据区域的关系。
执行 CREATE TABLESPACE 语句会在指定的目录下创建表空间。而在该目录下还会创建版本特定的子目录(例如 PG_16_202307071
)。版本特定的命名方式为:
PG_主版本号_目录版本号
举个例子,如果在 /home/postgres/tblspc
中创建一个表空间 new_tblspc
,其oid为 304152
,则会在表空间下创建一个名如 PG_16_202307071
的子目录。
#postgres>
mkdir -p /home/postgres/tblspc/
psql testdb postgres
CREATE TABLESPACE new_tblspc LOCATION '/home/postgres/tblspc';
\db+
List of tablespaces
Name | Owner | Location | Access privileges | Options | Size | Description
------------+----------+-----------------------+-------------------+---------+---------+-------------
new_tblspc | postgres | /home/postgres/tblspc | | | 0 bytes |
pg_default | postgres | | | | 5097 MB |
pg_global | postgres | | | | 669 kB |
(3 rows)
#postgres>
ls -l /home/postgres/tblspc/
drwx------ 2 postgres postgres 6 Jul 10 16:06 PG_16_202307071
表空间目录通过pg_tblspc
子目录中的符号链接寻址,链接名称与表空间的OID值相同。
#postgres>
ls -l $PGDATA/pg_tblspc/
lrwxrwxrwx 1 postgres postgres 21 Jul 10 16:06 304152 -> /home/postgres/tblspc
如果在该表空间下创建新的数据库(OID
为304153
),则会在版本特定的子目录下创建相应目录。
#postgres>
psql -U postgres -d testdb
CREATE DATABASE new_db TABLESPACE new_tblspc ;
#postgres>
ls -l /home/postgres/tblspc/PG_16_202307071/
total 28
drwx------ 2 postgres postgres 12288 Jul 10 16:13 304153
如果在该表空间内创建一个新表,但新表所属的数据库却创建在基础目录下,那么 PostgreSQL 会首先在版本特定的子目录下创建名称与现有数据库OID相同的新目录,然后将新表文件放置在刚创建的目录下。
CREATE TABLE newtbl(id int) TABLESPACE new_tblspc;
SELECT pg_relation_filepath('newtbl');
pg_relation_filepath
----------------------------------------------
pg_tblspc/304152/PG_16_202307071/16384/304154
为什么需要在数据库集簇目录下添加链接文件?
为了备份和管理的时候方便管理。如果没有链接文件将会导致数据文件的混乱。
3.3 堆表数据文件内部物理结构
在数据文件(堆表,索引,也包括 FSM
文件和 VM
文件)内部,它被划分为固定长度的页(pages),或称之为 区块(blocks),大小默认为 8192
字节 (8KB)
。
每个文件中的页从 0
开始按顺序编号,这些数字称为区块号(block numbers)。如果当前数据块已填满,PostgreSQL 通过在数据文件末尾添加一个新的空数据块用于扩展文件。
表的页面包含了三种类型的数据:
-
堆元组(heap tuples)
堆元组就是数据记录本身。它们从页面底部开始依序增长和堆叠。并发控制 与 WAL 日志 会介绍元组的内部结构,这一知识对于理解 PostgreSQL 并发控制 与 WAL 日志机制是必须的。 -
行指针(line pointer) —— 每个行指针占4个字节,保存着指向堆元组的指针。它们也被称为项目指针(item pointer)。行指针简单地组织为一个数组,扮演了元组索引的角色。每个索引项从1开始依次编号,称为偏移号(offset number)。当向页面中添加新元组时,一个相应的新行指针也会被放入数组中,并指向新添加的元组。
-
首部数据(header data) —— 页面的起始位置分配了由结构
PageHeaderData
定义的首部数据。它的大小为24个字节,包含关于页面的元数据。该结构的主要成员变量为:pd_lsn
—— 本页面最近一次变更所写入XLOG记录对应的LSN。它是一个8字节无符号整数,与WAL机制相关,第9章将详细展开。pd_checksum
—— 本页面的校验和值。(注意只有在9.3或更高版本才有此变量,早期版中该字段用于存储页面的时间线标识)pd_lower
,pd_upper
——pd_lower
指向行指针的末尾,pd_upper
指向最新堆元组的起始位置。pd_special
—— 在索引页中会用到该字段。在堆表页中它指向页尾。(在索引页中它指向特殊空间的起始位置,特殊空间是仅由索引使用的特殊数据区域,包含特定的数据,具体内容依索引的类型而定,如B树,GiST,GiN等。
/* @src/include/storage/bufpage.h */ /* * 磁盘页面布局 * * 对任何页面都适用的通用空间管理信息 * * pd_lsn - 本页面最近变更对应xlog记录的标识。 * pd_checksum - 页面校验和 * pd_flags - 标记位 * pd_lower - 空闲空间开始位置 * pd_upper - 空闲空间结束位置 * pd_special - 特殊空间开始位置 * pd_pagesize_version - 页面的大小,以及页面布局的版本号 * pd_prune_xid - 本页面中可以修剪的最老的元组中的XID. * * 缓冲管理器使用LSN来强制实施WAL的基本规则:"WAL需先于数据写入"。直到xlog刷盘位置超过 * 本页面的LSN之前,不允许将缓冲区的脏页刷入磁盘。 * * pd_checksum 存储着页面的校验和,如果本页面配置了校验。0是一个合法的校验和值。如果页面 * 没有使用校验和,我们就不会设置这个字段的值;通常这意味着该字段值为0,但如果数据库是从早于 * 9.3版本从 pg_upgrade升级而来,也可能会出现非零的值。因为那时候这块地方用于存储页面最后 * 更新时的时间线标识。 注意,并没有标识告诉你页面的标识符到底是有效还是无效的,也没有与之关 * 联的标记为。这是特意设计成这样的,从而避免了需要依赖页面的具体内容来决定是否校验页面本身。 * * pd_prune_xid是一个提示字段,用于帮助确认剪枝是否有用。目前对于索引页没用。 * * 页面版本编号与页面尺寸被打包成了单个uint16字段,这是有历史原因的:在PostgreSQL7.3之前 * 并没有页面版本编号这个概念,这样做能让我们假装7.3之前的版本的页面版本编号为0。我们约束页面 * 的尺寸必须为256的倍数,留下低8位用于页面版本编号。 * * 最小的可行页面大小可能是64字节,能放下页的首部,空闲空间,以及一个最小的元组。当然在实践中 * 肯定要大得多(默认为8192字节),所以页面大小必需是256的倍数并不是一个重要限制。而在另一端, * 我们最大只能支持32KB的页面,因为 lp_off/lp_len字段都是15bit。 */ typedef struct PageHeaderData { PageXLogRecPtr pd_lsn; /* 最近应用至本页面XLog记录的LSN */ uint16 pd_checksum; /* 校验和 */ uint16 pd_flags; /* 标记位,详情见下 */ LocationIndex pd_lower; /* 空闲空间起始位置 */ LocationIndex pd_upper; /* 空闲空间终止位置 */ LocationIndex pd_special; /* 特殊用途空间的开始位置 */ uint16 pd_pagesize_version; TransactionId pd_prune_xid; /* 最老的可修剪XID, 如果没有设置为0 */ ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* 行指针的数组 */ } PageHeaderData; /* 缓冲区页中的项目指针(item pointer),也被称为行指针(line pointer)。 * * 在某些情况下,项目指针处于 “使用中”的状态,但在本页中没有任何相关联的存储区域。 * 按照惯例,lp_len == 0 表示该行指针没有关联存储。独立于其lp_flags的状态. */ typedef struct ItemIdData { unsigned lp_off:15, /* 元组偏移量 (相对页面起始处) */ lp_flags:2, /* 行指针的状态,见下 */ lp_len:15; /* 元组的长度,以字节计 */ } ItemIdData; /* lp_flags有下列可能的状态,LP_UNUSED的行指针可以立即重用,而其他状态的不行。 */ #define LP_UNUSED 0 /* unused (lp_len必需始终为0) */ #define LP_NORMAL 1 /* used (lp_len必需始终>0) */ #define LP_REDIRECT 2 /* HOT 重定向 (lp_len必需为0) */ #define LP_DEAD 3 /* 死元组,有没有对应的存储尚未可知 */
行指针的末尾与最新元组起始位置之间的空余空间称为空闲空间(free space)或空洞(hole)。
为了识别表中的元组,数据库内部会使用元组标识符(tuple identifier, TID)。TID由一对值组成:元组所属页面的区块号,及指向元组的行指针的偏移号。TID的一种典型用途是索引。
结构体 PageHeaderData
定义于 src/include/storage/bufpage.h
中。
此外,大小超过约2KB(8KB的四分之一)的堆元组会使用一种称为 TOAST(The Oversized-Attribute Storage Technique,超大属性存储技术) 的方法来存储与管理。
- 数据字典默认为小写存储
3.4 VM 和FSM
fsm : free space map
vm : visualbel map
4 元组的读写方式
4.1 写入堆元组
让我们假设有一个表,仅由一个页面组成,且该页面只包含一个堆元组。 此页面的pd_lower指向第一个行指针,而该行指针和pd_upper都指向第一个堆元组。 如图1.5(a)所示。
当第二个元组被插入时,它会被放在第一个元组之后。第二个行指针被插入到第一个行指针的后面,并指向第二个元组。 pd_lower更改为指向第二个行指针,pd_upper更改为指向第二个堆元组,如图1.5(b)。页面内的首部数据(例如pd_lsn,pg_checksum,pg_flag)也会被改写为适当的值,细节在第5.3节和第9章中描述。
4.2 读取堆元组
5 数据块、wal段、数据文件大小修改
数据块按8K划分(—with-blocksize,单位为K,默认为8K)
WAL块尺寸按8k划分(—with-wal-blocksize,单位为K,默认为8K)
数据文件按1G一个分解(—with-segsize,单位为G,默认为1G)
WAL段尺寸按16MB划分(—with-wal-segsize,单位为K,默认为16MB)
重新编译
#postgresql>
./configure —with-blocksize=8—with-wal-blocksize=8—with-segsize=1 —with-wal-segsize=16
查看编译信息
#postgresql>
pg_config —configure